﻿using System;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Linq.Dynamic;
using System.Linq.Expressions;
using System.Threading;
using Infrastructure.Messages;
using Infrastructure.Persistence;
using Infrastructure.Utility;

namespace SocialFront.Core.Queries
{
    public abstract class Query<TQueryParameter, TQueryResult>
        where TQueryParameter : class
        where TQueryResult : class
    {
        private int _retryAttempts = 3;
        private int _delayInMilliSeconds;

        protected readonly IReadOnlyRepository Repository;
        private IQueryable<TQueryResult> _underlyingQuery;
        private bool _isAlreadyOrdered;

        protected Query(IReadOnlyRepository repository = null)
        {
            Repository = repository ?? Facade.ReadOnlyRepository;

            _isAlreadyOrdered = false;
        }

        public TQueryParameter Parameter { get; private set; }
        protected Action<long> CountCallback { get; set; }
        protected Func<Exception, bool> ErrorCallback { get; set; }
        protected abstract string DefaultSortExpression { get; }
        protected abstract void ValidateParameter();
        protected abstract IQueryable<TQueryResult> CreateQuery();

        protected IQueryable<TQueryResult> UnderlyingQuery
        {
            get
            {
                ensureParameterIsNotNull();
                ValidateParameter();
                return _underlyingQuery ?? (_underlyingQuery = CreateQuery());
            }
            set
            {
                if (value != null)
                {
                    _underlyingQuery = value;
                }
                else
                {
                    var propertyName = QueryName;
                    throw new ArgumentNullException(propertyName);
                }
            }
        }

        protected string QueryName
        {
            get
            {
                return this.GetPropertyName(() => UnderlyingQuery);
            }
        }

        private void ensureParameterIsNotNull()
        {
            if (Parameter == null)
            {
                var propertyName = this.GetPropertyName(() => UnderlyingQuery);
                throw new ArgumentNullException(propertyName + " - Parameter cannot be null.");
            }
        }

        public virtual long Count()
        {
            return UnderlyingQuery.Count();
        }

        #region Response Methods -- FirstOrDefault, First, etc.

        public virtual GetResponse<TQueryResult> FirstOrDefault()
        {
            var response = new GetResponse<TQueryResult>();

            var successful = false;
            var retryCount = 0;

            do
            {
                try
                {
                    response.Result = UnderlyingQuery.FirstOrDefault();
                    response.Success = true;
                    successful = true;
                }
                catch (EntityCommandExecutionException ex)
                {
                    retryCount = handleCommandExecutionException(ex, response, retryCount);
                }
                catch (Exception ex)
                {
                    retryCount = handleNonEntityCommandExecutionExceptions(ex, response);
                }
            } while (retryCount < _retryAttempts && !successful);

            return response;
        }

        public virtual GetResponse<TQueryResult> First()
        {
            var response = new GetResponse<TQueryResult>();

            var successful = false;
            var retryCount = 0;

            do
            {
                try
                {
                    response.Result = UnderlyingQuery.First();

                    response.Success = true;
                    successful = true;
                }
                catch (EntityCommandExecutionException ex)
                {
                    retryCount = handleCommandExecutionException(ex, response, retryCount);
                }
                catch (Exception ex)
                {
                    retryCount = handleNonEntityCommandExecutionExceptions(ex, response);
                }
            } while (retryCount < _retryAttempts && !successful);

            return response;
        }

        public virtual ListResponse<TQueryResult> PageOfResults(int pageNumber = 1, int pageSize = 20)
        {
            var skip = (pageNumber - 1) * pageSize;
            var take = pageSize;

            if (!_isAlreadyOrdered && DoesQueryNeedOrdering(UnderlyingQuery))
                UnderlyingQuery = ApplyDefaultOrdering(UnderlyingQuery);

            var response = new ListResponse<TQueryResult>();

            var successful = false;
            var retryCount = 0;

            do
            {
                try
                {
                    if (pageSize == int.MaxValue)
                        response.List = UnderlyingQuery.ToList();
                    else
                        response.List = UnderlyingQuery.Skip(skip).Take(take).ToList();

                    successful = true;
                    response.Success = true;
                }
                catch (EntityCommandExecutionException ex)
                {
                    retryCount = handleCommandExecutionException(ex, response, retryCount);
                }
                catch (Exception ex)
                {
                    retryCount = handleNonEntityCommandExecutionExceptions(ex, response);
                }
            } while (retryCount < _retryAttempts && !successful);

            return response;
        }

        private int handleNonEntityCommandExecutionExceptions(Exception ex, ResponseBase response)
        {
            response.Success = false;
            response.Message = this.GetPropertyName(() => this, false) + ex.Message;

            if (ErrorCallback != null)
                ErrorCallback(ex);

            // We don't want to keep going for regular exceptions just command execute exceptions (deadlocks)
            return _retryAttempts;
        }

        private int handleCommandExecutionException(Exception ex,
                                                    ResponseBase response,
                                                    int retryCount)
        {
            Thread.Sleep(_delayInMilliSeconds);

            if (retryCount == _retryAttempts)
            {
                if (ErrorCallback != null)
                    ErrorCallback(ex);

                response.Success = false;
                response.Message = ex.Message;
            }
            return ++retryCount;
        }

        #endregion

        protected virtual bool DoesQueryNeedOrdering(IQueryable<TQueryResult> query)
        {
            var type = query.Expression.Type;

            //NOTE: 
            //If an EF query is not ordered, type will be ObjectQuery<>, and if it is will be IOrderedQueryable<>.
            //The EnumerableQuery check is allowing us to have a non-EF IQueryable<> such as when we unit test 
            //and use an in-memory IQueryable<>. In memory IQueryable<> is of type EnumerableQuery. We want to
            //check for all assignable types because we could have our own derived class from EnumerableQuery when
            //we do unit tests that use in-memory IQueryable<> stores.

            //TODO:
            //IsAssignableFrom is expensive. Consider keeping it only in the debug builds since it is only needed
            //for unit testing.

            var isOrderingRequired = type != typeof(IOrderedQueryable<TQueryResult>); // && !type.IsAssignableFrom(typeof(EnumerableQuery));

            return isOrderingRequired;
        }

        protected virtual IQueryable<TQueryResult> ApplyDefaultOrdering(IQueryable<TQueryResult> query)
        {
            if (DefaultSortExpression == null)
                throw new Exception("Default Sort Expression required");

            query = query.OrderBy(DefaultSortExpression);

            return query;
        }

        public Query<TQueryParameter, TQueryResult> Where(TQueryParameter parameter)
        {
            Parameter = parameter;
            return this;
        }

        public Query<TQueryParameter, TQueryResult> Where(Expression<Func<TQueryResult, bool>> expression)
        {
            UnderlyingQuery = UnderlyingQuery.Where(expression);
            return this;
        }

        public virtual Query<TQueryParameter, TQueryResult> Include(string path)
        {
            UnderlyingQuery = UnderlyingQuery.Include(path);
            return this;
        }

        public virtual Query<TQueryParameter, TQueryResult> Include<TProperty>(
            Expression<Func<TQueryResult, TProperty>> path)
        {
            UnderlyingQuery = UnderlyingQuery.Include(path);
            return this;
        }

        public Query<TQueryParameter, TQueryResult> WithRetry(int retryAttempts = 0, int delayInMilliseconds = 500)
        {
            _retryAttempts = retryAttempts;
            _delayInMilliSeconds = delayInMilliseconds;

            return this;
        }

        public virtual Query<TQueryParameter, TQueryResult> OrderBy<TKey>(Expression<Func<TQueryResult, TKey>> keySelector)
        {
            UnderlyingQuery = UnderlyingQuery.OrderBy(keySelector);
            _isAlreadyOrdered = true;
            return this;
        }

        public virtual Query<TQueryParameter, TQueryResult> OrderBy(string expression)
        {
            UnderlyingQuery = UnderlyingQuery.OrderBy(expression);
            _isAlreadyOrdered = true;
            return this;
        }

        public virtual Query<TQueryParameter, TQueryResult> OrderByDescending<TKey>(Expression<Func<TQueryResult, TKey>> keySelector)
        {
            UnderlyingQuery = UnderlyingQuery.OrderByDescending(keySelector);
            _isAlreadyOrdered = true;
            return this;
        }

        public virtual Query<TQueryParameter, TQueryResult> OrderByDescending(string sortExpression)
        {
            UnderlyingQuery = UnderlyingQuery.OrderBy(sortExpression + " DESC");
            _isAlreadyOrdered = true;
            return this;
        }

        public virtual Query<TQueryParameter, TQueryResult> ThenByDescending<TKey>(Expression<Func<TQueryResult, TKey>> keySelector)
        {
            UnderlyingQuery = (UnderlyingQuery as IOrderedQueryable<TQueryResult>).ThenByDescending(keySelector);
            _isAlreadyOrdered = true;
            return this;
        }

        // TODO - Is this needed?
        public virtual Query<TQueryParameter, TQueryResult> ThenByDescending(string sortExpression)
        {
            _isAlreadyOrdered = true;
            return this;
        }

        public virtual Query<TQueryParameter, TQueryResult> ThenBy<TKey>(Expression<Func<TQueryResult, TKey>> keySelector)
        {
            UnderlyingQuery = (UnderlyingQuery as IOrderedQueryable<TQueryResult>).ThenBy(keySelector);
            _isAlreadyOrdered = true;
            return this;
        }

        public virtual Query<TQueryParameter, TQueryResult> ThenBy(string sortExpression)
        {
            _isAlreadyOrdered = true;
            return this;
        }

        public virtual Query<TQueryParameter, TQueryResult> OnCount(Action<long> countCallback)
        {
            countCallback(Count());
            return this;
        }

        public virtual Query<TQueryParameter, TQueryResult> OnError(Func<Exception, bool> errorCallback)
        {
            ErrorCallback = errorCallback;
            return this;
        }

        //public virtual IQuery<TQueryParameter, TQueryResult> OnValidationError(Func<ValidationException, bool> errorCallback)
        //{
        //    this.ValidationErrorCallback = errorCallback;
        //    return this;
        //}
    }
}
